/*
 * MegaMek - Copyright (C) 2000-2003 Ben Mazur (bmazur@sev.org)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 */
package megamek.client.bot;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import megamek.client.bot.ga.Chromosome;
import megamek.client.bot.ga.GA;
import megamek.common.Compute;
import megamek.common.Entity;
import megamek.common.IGame;
import megamek.common.Mech;
import megamek.common.Terrains;
import megamek.common.ToHitData;

// TODO: Auto-generated Javadoc
/**
 * Need to test the function that moves all firing to a single target.
 */
public class GAAttack extends GA {

    /** The attack. */
    protected transient List<ArrayList<AttackOption>> attack; // NOPMD by Adj1031 on 5/18/14 12:04 PM
    
    /** The attacker. */
    protected transient CEntity attacker; // NOPMD by Adj1031 on 5/18/14 12:05 PM
    
    /** The game. */
    protected transient IGame game; // NOPMD by Adj1031 on 5/18/14 12:05 PM
    
    /** The targets. */
    protected transient CEntity.Table targets; // NOPMD by Adj1031 on 5/18/14 12:05 PM
    
    /** The target_array. */
    protected transient List<Entity> targetArray = null; // NOPMD by Adj1031 on 5/18/14 12:01 PM
    
    /** The valid_target_indexes. */
    protected List<Integer> validTarget = null; // NOPMD by Adj1031 on 5/18/14 12:01 PM
    
    /** The overheat_eligible. */
    protected boolean overheatEligible = false; // NOPMD by Adj1031 on 5/18/14 12:02 PM
    
    /** The firing_arc. */
    protected int firingArc = 0; // NOPMD by Adj1031 on 5/18/14 12:02 PM
    
    /** The damages. */
    transient double[] damages = null;

    /**
     * Instantiates a new GA attack.
     *
     * @param testBot the tb
     * @param attacker the attacker
     * @param attack the attack
     * @param population the population
     * @param generations the generations
     * @param isEnemy the is enemy
     */
    public GAAttack(final TestBot testBot, final CEntity attacker, // NOPMD by Adj1031 on 5/18/14 12:06 PM
            final List<ArrayList<AttackOption>> attack, final int population,
            final int generations, final boolean isEnemy) {
        super(attack.size() + 1, population, .7, .05, generations, .4);
        this.attack = attack;
        this.attacker = attacker;
        game = testBot.game;
        targetArray = new ArrayList<Entity>(game.getEntitiesVector());
        final ArrayList<Integer> temp = new ArrayList<Integer>();
        for (int i = 0; i < targetArray.size(); i++) {
            final Entity entity = targetArray.get(i);
            if (entity.isEnemyOf(attacker.entity) && entity.isDeployed()) {
                temp.add(i); // NOPMD by Adj1031 on 5/18/14 12:03 PM
            }
        }
        targets = new CEntity.Table(testBot);
        validTarget = temp;
        if (attacker.tsm_offset) {
            overheatEligible = true;
        }
        if (isEnemy
                || (attacker.last != null && (!attacker.last.inDanger || attacker.last.doomed))) {
            overheatEligible = true;
        }
    }

    /**
     * Gets the result chromosome.
     *
     * @return the result chromosome
     */
    public final int[] getResultChromosome() {
        return chromosomes[populationDim - 1].genes;
    }

    /**
     * Gets the damage utility.
     *
     * @param entity the to
     * @return the damage utility
     */
    public final double getDamageUtility(final CEntity entity) {
        if (damages == null) {
            damages = getDamageUtilities();
        }
        for (int k = 0; k < targetArray.size(); k++) {
            final Entity enemy = targetArray.get(k);
            if (enemy.getId() == entity.entity.getId()) {
                return damages[k];
            }
        }
        return 0;
    }

    /**
     * Gets the damage utilities.
     *
     * @return the damage utilities
     */
    public final double[] getDamageUtilities() {
        final int iChromIndex = populationDim - 1;
        targets.clear(); // could use ArrayList and not hashtable
        double[] result = new double[targetArray.size()];
        final Chromosome chromArrayList = chromosomes[iChromIndex];
        // TODO should account for high heat?
        int heat_total = 0;
        if (chromArrayList.genes[chromosomeDim - 1] >= targetArray.size()) {
            chromArrayList.genes[chromosomeDim - 1] = validTarget.get(
                    0).intValue();
        }
        final Entity target = targetArray
                .get(chromArrayList.genes[chromosomeDim - 1]);
        for (int iGene = 0; iGene < chromosomeDim - 1; iGene++) {
            AttackOption attackOptionA = attack.get(iGene).get(chromArrayList.genes[iGene]);
            if (attackOptionA.target != null) { // if not the no fire option
                targets.put(attackOptionA.target);
                double mod = 1;
                if (attackOptionA.target.entity.getId() == target.getId()) {
                    attackOptionA.target.possible_damage[attackOptionA.toHit.getSideTable()] += mod
                            * attackOptionA.primaryExpected;
                } else {
                    attackOptionA.target.possible_damage[attackOptionA.toHit.getSideTable()] += mod
                            * attackOptionA.expected;
                }
                heat_total += attackOptionA.heat;
            }
        }

        for (int k = 0; k < targetArray.size(); k++) {
            final Entity entityA = targetArray.get(k);
            CEntity enemy = null;
            result[k] = 0;
            enemy = targets.get(Integer.valueOf(entityA.getId()));
            if (enemy != null) { // NOPMD by Adj1031 on 5/18/14 12:03 PM
                result[k] = getThreadUtility(enemy);
                enemy.resetPossibleDamage();
            }
        }
        return result;
    }

    /**
     * Gets the thread utility.
     *
     * @param enemy the enemy
     * @return the thread utility
     */
    private double getThreadUtility(final CEntity enemy) {
        if (enemy.possible_damage[ToHitData.SIDE_FRONT] > 0) {
            return enemy.getThreatUtility(
                    enemy.possible_damage[ToHitData.SIDE_FRONT],
                    ToHitData.SIDE_FRONT);
        } else if (enemy.possible_damage[ToHitData.SIDE_REAR] > 0) {
            return enemy.getThreatUtility(
                    enemy.possible_damage[ToHitData.SIDE_REAR],
                    ToHitData.SIDE_REAR);
        } else if (enemy.possible_damage[ToHitData.SIDE_LEFT] > 0) {
            return enemy.getThreatUtility(
                    enemy.possible_damage[ToHitData.SIDE_LEFT],
                    ToHitData.SIDE_LEFT);
        } else if (enemy.possible_damage[ToHitData.SIDE_RIGHT] > 0) {
            return enemy.getThreatUtility(
                    enemy.possible_damage[ToHitData.SIDE_RIGHT],
                    ToHitData.SIDE_RIGHT);
        }
        return 0;
    }

    /* (non-Javadoc)
     * @see megamek.client.bot.ga.GA#getFitness(int)
     */
    @Override
	protected final double getFitness(final int iChromIndex) {
        return this.getFitness(chromosomes[iChromIndex]);
    }

    /**
     * Gets the fitness.
     *
     * @param chromArrayList the chrom array list
     * @return the fitness
     */
    protected final double getFitness(final Chromosome chromArrayList) {
        targets.clear(); // could use ArrayList and not hashtable
        int heat_total = 0;
        Entity target = null;
        try {
            target = targetArray.get(chromArrayList.genes[chromosomeDim - 1]);
        } catch (Exception e) {
            System.out.println(chromosomeDim // NOPMD by Adj1031 on 5/18/14 12:04 PM
                    + " " + chromArrayList.genes.length); //$NON-NLS-1$
            System.out.println(targetArray.size()); // NOPMD by Adj1031 on 5/18/14 12:04 PM
            target = targetArray.get(validTarget.get(0).intValue());
        }
        for (int iGene = 0; iGene < chromosomeDim - 1; iGene++) {
            final int[] genes = chromArrayList.genes;
            AttackOption attackOptionB = attack.get(iGene).get(genes[iGene]);
            if (attackOptionB.target != null) { // if not the no fire option
                targets.put(attackOptionB.target);
                double mod = 1;
                if (attackOptionB.primaryOdds <= 0) {
                    mod = 0; // If there's no chance to hit at all...
                } else if (attackOptionB.ammoLeft != -1) {
                    if (attacker.overall_armor_percent < .5) {
                        mod = 1.5; // get rid of it
                    } else if (attackOptionB.ammoLeft < 12
                            && attacker.overall_armor_percent > .75) {
                        if (attackOptionB.primaryOdds < .1) {
                            mod = 0;
                        } else if (attackOptionB.ammoLeft < 6 && attackOptionB.primaryOdds < .25) {
                            mod = 0;
                        } else {
                            mod = attackOptionB.primaryOdds; // low percentage shots will
                            // be frowned upon
                        }
                    }
                }
                if (attackOptionB.target.entity.getId() == target.getId()) {
                    attackOptionB.target.possible_damage[attackOptionB.toHit.getSideTable()] += mod
                            * attackOptionB.primaryExpected;
                } else {
                    attackOptionB.target.possible_damage[attackOptionB.toHit.getSideTable()] += mod
                            * attackOptionB.expected;
                }
                heat_total += attackOptionB.heat;
            }
        }
        double total_utility = 0;
        final Iterator<CEntity> iteratorEntity = targets.values().iterator();
        while (iteratorEntity.hasNext()) {
            final CEntity enemy = iteratorEntity.next();
            total_utility += getThreadUtility(enemy);
            enemy.resetPossibleDamage();
        }
        // should be moved
        final int capacity = attacker.entity.getHeatCapacityWithWater();
        final int currentHeat = attacker.entity.heatBuildup + attacker.entity.heat;
        int overheat = currentHeat + heat_total - capacity;
        // Don't forget heat from stealth armor...
        if (attacker.entity instanceof Mech
                && (attacker.entity.isStealthActive()
                        || attacker.entity.isNullSigActive() || attacker.entity
                        .isVoidSigActive())) {
            overheat += 10;
        }
        // ... or chameleon lps...
        if (attacker.entity instanceof Mech
                && attacker.entity.isChameleonShieldActive()) {
            overheat += 6;
        }
        // ... or infernos...
        if (attacker.entity.infernos.isStillBurning()) {
            overheat += 6;
        }
        // ... or standing in fire...
        if (game.getBoard().getHex(attacker.entity.getPosition()) != null && game.getBoard().getHex(attacker.entity.getPosition())
                    .containsTerrain(Terrains.FIRE)
                    && game.getBoard().getHex(attacker.entity.getPosition())
                            .getFireTurn() > 0) {
                overheat += 5;
            }
        // ... or from engine hits
        if (attacker.entity instanceof Mech) {
            overheat += attacker.entity.getEngineCritHeat();
        }
        // ... or ambient temperature
        overheat += game.getPlanetaryConditions().getTemperatureDifference(50,
                -30);
        if (attacker.entity.heat > 0 && overheat < 0) {
            // always perfer smaller heat numbers
            total_utility -= attacker.bv / 1000 * overheat;
            // but add clear deliniations at the breaks
            if (attacker.entity.heat > 4) {
                total_utility *= 1.2;
            }
            if (attacker.entity.heat > 7) {
                total_utility += attacker.bv / 50;
            }
            if (attacker.tsm_offset) {
                if (attacker.entity.heat == 9) {
                    total_utility -= attacker.bv / 10;
                }
                if (attacker.entity.heat < 12 && attacker.entity.heat > 9) {
                    total_utility -= attacker.bv / 20;
                }
            }
            if (attacker.entity.heat > 12) {
                total_utility += attacker.bv / 20;
            }
            if (attacker.entity.heat > 16) {
                total_utility += attacker.bv / 10;
            }
        } else if (overheat > 0) {
            if (overheat > 4 && !attacker.tsm_offset) {
                total_utility *= (overheatEligible && attacker.jumpMP > 2) ? .9
                        : .85;
            }
            if (overheat > 7 && !attacker.tsm_offset) {
                final double mod = overheatEligible ? +((attacker.jumpMP > 2) ? 0
                        : 10) : 40;
                if (attacker.overheat > CEntity.OVERHEAT_LOW) {
                    total_utility -= attacker.bv / mod;
                } else {
                    total_utility -= attacker.bv / (mod + 10);
                }
            }
            if (attacker.tsm_offset) {
                if (overheat == 9) {
                    total_utility += attacker.bv / 10;
                }
                if (attacker.entity.heat < 12 && attacker.entity.heat > 9) {
                    total_utility += attacker.bv / 20;
                }
            }
            if (overheat > 12) {
                total_utility -= attacker.bv / (overheatEligible ? 45 : 30);
            }
            if (overheat > 16) {
                // only if I am going to die?
                total_utility -= attacker.bv / 5;
            }
            total_utility -= overheat / 100; // small preference for less
            // overheat opposed to more
        }
        return total_utility;
    }

    /**
     * since the low fitness members have the least chance of getting selected,
     * but the highest chance of mutation, this is where we use the primary
     * target heuristic to drive convergence.
     *
     * @param iChromIndex the i chrom index
     */
    @Override
	protected final void doRandomMutation(final int iChromIndex) {
        Chromosome chromoOne = chromosomes[iChromIndex];
        // skip if it's an empty chromosome
        if (chromoOne.genes.length < 1) {
            return;
        }
        final int randomOne = (chromoOne.genes.length > 2) ? Compute.randomInt(chromoOne.genes.length - 1)
                : 0;
        CEntity target = null;
        boolean done = false;
        if (randomOne % 2 != 0) {
            chromoOne.genes[randomOne]--;
            if (chromoOne.genes[randomOne] < 0 && attack.size() > randomOne) {
                chromoOne.genes[randomOne] = attack.get(randomOne).size() - 1;
            } else {
                chromoOne.genes[randomOne] = 0; // TODO : what is a good value here?
            }
            return;
        }
        // else try to move all to one target
        for (int i = 0; (i < chromoOne.genes.length - 1) && !done; i++) {
            final int iGene = (i + randomOne) % (chromoOne.genes.length - 1);
            final AttackOption attackOptionA = attack.get(iGene).get(chromoOne.genes[iGene]);
            if (attackOptionA.target != null) {
                target = attackOptionA.target;
                done = true;
            }
        }
        if (target == null) { // then not shooting, so shoot something
            if (attack.size() > randomOne && randomOne > 1) {
                chromoOne.genes[randomOne] = Compute.randomInt(attack.get(randomOne).size() - 1);
            } else {
                // TODO : Is this the correct action to take?
                chromoOne.genes[randomOne] = Compute.randomInt(attack.get(0).size() - 1);
            }
            final AttackOption attackOptionB = attack.get(randomOne).get(chromoOne.genes[randomOne]);
            if (attackOptionB.target != null) {
                chromoOne.genes[chromoOne.genes.length - 1] = attackOptionB.target.enemy_num;
            }
        } else { // let's switch as many attacks as we can to this guy
            for (int i = 0; (i < (chromoOne.genes.length - 1)) && (i < attack.size()); i++) {
                final Object[] weapon = attack.get(i).toArray();
                if (chromoOne.genes[i] != weapon.length - 1) {
                    done = false;
                    for (int w = 0; (w < weapon.length - 1) && !done; w++) {
                        final AttackOption attackOptionA = (AttackOption) weapon[w];
                        if (attackOptionA.target.enemy_num == target.enemy_num) {
                            chromoOne.genes[i] = w;
                            done = true;
                        }
                    }
                }
            }
            (chromosomes[0]).genes[chromosomeDim - 1] = target.enemy_num;
        }
    }

    /* (non-Javadoc)
     * @see megamek.client.bot.ga.GA#initPopulation()
     */
    @Override
	protected final void initPopulation() {
        // promote max
        for (int iGene = 0; iGene < chromosomeDim - 1; iGene++) {
            (chromosomes[0]).genes[iGene] = 0;
        }

        // use first weapon target as primary, not smart but good enough...
        final AttackOption atackOptionB = attack.get(0).get(0);
        (chromosomes[0]).genes[chromosomeDim - 1] = atackOptionB.target.enemy_num;

        for (int i = 1; i < populationDim; i++) {
            final Chromosome chromoV = chromosomes[i];
            for (int iGene = 0; iGene < chromosomeDim - 1; iGene++) {
                chromoV.genes[iGene] = Compute.randomInt(attack.get(iGene).size());
                if (i <= attack.size()) {
                    if (iGene + 1 == i) {
                        chromoV.genes[iGene] = 0; // fire
                    } else {
                        chromoV.genes[iGene] = attack.get(iGene).size() - 1;
                    }
                }
            }
            chromoV.genes[chromosomeDim - 1] = validTarget.get(
                    Compute.randomInt(validTarget.size())).intValue();
            chromosomes[i].fitness = getFitness(i);
        }
    }

    /**
     * Gets the firing arc.
     *
     * @return the firing arc
     */
    public final int getFiringArc() {
        return firingArc;
    }

    /**
     * Sets the firing arc.
     *
     * @param firing_arc the new firing arc
     */
    public final void setFiringArc(final int firing_arc) {
        this.firingArc = firing_arc;
    }

    /**
     * Gets the attack.
     *
     * @return the attack
     */
    public final List<ArrayList<AttackOption>> getAttack() {
        return attack;
    }

}
